28 — Shader 图案

Three.js Journey · Bruno Simon · 难度:简单

28 — Shader 图案课程海报

简介

在创建 shader 时,我们常常需要绘制特定的图案,比如星星、圆形、光晕透镜、波浪等。

这有助于在 geometry 上直观地看到这些图案,也可以用来移动顶点,就像我们在上一课处理旗帜时做的那样。

我们本可以使用 texture,但绘制形状能给我们更多控制权;我们可以对形状参数做动画,而且无需加载 texture。

这比使用 canvas 等其他 API 来绘制要复杂得多,因为每个 fragment 运行的代码都相同,我们拥有的只有坐标和我们自己的数学能力。

是的,这一课会涉及一些数学。对某些人来说这是最令人沮丧的部分之一,但别怕;即使你数学不太好,你也能找到解决办法。

在这一课中,我们将尝试在一个平面上绘制各种图案。我们会从非常细致的部分开始,随后难度会逐渐增加。这是了解经典技巧并使用内置函数的绝佳机会。

对于每个图案,我们先研究结果,然后尝试复现它。如果你想在这方面更上一层楼,请在每个图案处暂停课程,自己动手尝试。即使失败,只要你自己尝试过,解决方案也会更有意义。

设置

目前,scene 上只有一个平面,使用 ShaderMaterial 作为 PlaneGeometry。提醒一下,ShaderMaterial 类似于 RawShaderMaterial,只是在 shader 前面预置了一些代码,比如导入矩阵、导入一些 attribute,或设置精度。

将 UV 坐标发送到 fragment

因为我们要绘制平面图案,所以大部分代码都会放在 fragment shader 中。但首先,我们需要把 UV 坐标从 vertex shader 发送到 fragment shader。

要在 vertex shader 中获取 uv attribute,我们本应写成类似这样:

attribute vec2 uv;

但由于我们使用的是 ShaderMaterial,这段代码已经预置在 vertex shader 中了。

要把这个值从 vertex shader 发送到 fragment shader,我们需要一个 varying。我们将它命名为 vUv,并把 uv 赋值给它:

varying vec2 vUv;

void main()
{
    // ...

    vUv = uv;
}

在 fragment shader 中,我们可以用相同的声明获取这个 vUv varying:

varying vec2 vUv;

void main()
{
    // ...
}

现在我们可以在 fragment shader 中通过 vUv 访问 uv 坐标了。其值从左下角的 0, 0 到右上角的 1, 1。

图案 1

这个漂亮的彩色图案是最容易实现的。我们只需在 gl_FragColor 中使用 vUv,并把蓝色分量设为 1.0:

varying vec2 vUv;

void main()
{
    gl_FragColor = vec4(vUv, 1.0, 1.0);
}

图案 2

这个图案完全相同,只是蓝色分量设为 0.0:

varying vec2 vUv;

void main()
{
    gl_FragColor = vec4(vUv, 0.0, 1.0);
}

图案 3

这里开始变得有趣一些了。为了得到这个渐变,我们只使用 vUv 的 x 分量,但把它用在 gl_FragColor 的前三个分量上:

varying vec2 vUv;

void main()
{
    gl_FragColor = vec4(vUv.x, vUv.x, vUv.x, 1.0);
}

从现在起,我们将绘制这样的黑白图案。与其把值分别赋给 r、g 和 b,我们可以创建一个名为 strength 的 float 变量:

varying vec2 vUv;

void main()
{
    float strength = vUv.x;

    gl_FragColor = vec4(vec3(strength), 1.0);
}

现在我们将专注于 strength 变量,尝试绘制接下来的图案。

与其替换掉之前的图案,你可以把它注释掉,这样之后还能回到它。

图案 4

这个图案完全相同,只是改用 y 轴:

float strength = vUv.y;

图案 5

这个图案完全相同,只是我们用 1.0 - ... 把值反转了:

float strength = 1.0 - vUv.y;

图案 6

要像这样压缩渐变,我们只需把值相乘。strength 会迅速跳到 1,但颜色不可能比白色更亮,所以渐变的其余部分会保持白色:

float strength = vUv.y * 10.0;

图案 7

这才像话。为了重复渐变,我们使用取模。取模运算求的是第一个数除以第二个数后的余数。

就好像第一个数一旦达到第二个数就回到 0 一样。

在许多语言中,我们可以用 % 来做取模,但在 GLSL 中必须使用 mod(...) 函数:

float strength = mod(vUv.y * 10.0, 1.0);

图案 8

这个图案看起来基于上一个,但这里不是渐变,而是 0.0 或 1.0。

我们本可以用 if 语句实现——因为条件语句在 GLSL 中确实能用——但出于性能考虑,我建议避免使用条件语句。

我们可以使用 step(...) 函数。第一个参数是边值,第二个参数是一个数。如果这个数小于边值,结果为 0.0;如果大于边值,结果为 1.0:

float strength = mod(vUv.y * 10.0, 1.0);
strength = step(0.5, strength);

如你所见,我们在另一行使用了 step(...) 函数并重新赋值给 strength。这不会带来性能损耗。你会看到许多 shader 开发者写出巨大的单行代码,尽可能少用变量、几乎不写注释,但那只是因为他们清楚自己在做什么。

随你喜好来做,尤其是如果你是初学者。

图案 9

这个图案与上一个相同,只是 step(...) 的边值更大:

float strength = mod(vUv.y * 10.0, 1.0);
strength = step(0.8, strength);

图案 10

这个图案与上一个相同,只是我们使用了 vUv 的 x 轴而非 y 轴:

float strength = mod(vUv.x * 10.0, 1.0);
strength = step(0.8, strength);

图案 11

我们也可以把它们组合起来。这里,我们需要把 x 轴的结果加到 y 轴的结果上:

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));

图案 12

这个图案使用相同的原理,只是改用乘法。我们只能看到它们的相交部分:

float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength *= step(0.8, mod(vUv.y * 10.0, 1.0));

图案 13

这个图案与之前相同,只是我们调整了 x 轴上 step 的边值:

float strength = step(0.4, mod(vUv.x * 10.0, 1.0));
strength *= step(0.8, mod(vUv.y * 10.0, 1.0));

图案 14

这个图案是之前几个图案的组合。我们在 x 轴上创建条,再加上 y 轴上的条:

float strength = step(0.4, mod(vUv.x * 10.0, 1.0)) * step(0.8, mod(vUv.y * 10.0, 1.0));
strength += step(0.8, mod(vUv.x * 10.0, 1.0)) * step(0.4, mod(vUv.y * 10.0, 1.0));

就像在任何语言中一样,当代码变得像这样难以忍受时,稍微重构一下是个好主意:

float barX = step(0.4, mod(vUv.x * 10.0, 1.0)) * step(0.8, mod(vUv.y * 10.0, 1.0));
float barY = step(0.8, mod(vUv.x * 10.0, 1.0)) * step(0.4, mod(vUv.y * 10.0, 1.0));
float strength = barX + barY;

图案 15

这个图案与之前相同,只是我们对条的 x 和 y 轴应用了一个小的偏移:

float barX = step(0.4, mod(vUv.x * 10.0 - 0.2, 1.0)) * step(0.8, mod(vUv.y * 10.0, 1.0));
float barY = step(0.8, mod(vUv.x * 10.0, 1.0)) * step(0.4, mod(vUv.y * 10.0 - 0.2, 1.0));
float strength = barX + barY;

这正是像我们这样的初学者会不断调整数值直到奏效的情况。这没什么问题,而且一旦你找到答案,解决方案大概就会变得合理。

图案 16

这个我们换个方向。为了得到这个结果,我们首先需要偏移 vUv.x,让它从 -0.5 到 0.5。然后我们需要这个值始终为正,于是它从 0.5 到 0.0 再到 0.5。为此,我们可以使用 abs(...) 函数:

float strength = abs(vUv.x - 0.5);

图案 17

这个图案看起来像是把上一个图案与 y 轴上的一个变体组合起来。这可不是普通的组合。你在这里看到的是 x 轴图案与 y 轴图案之间的最小值。为此,我们使用 min(...) 函数:

float strength = min(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

图案 18

与上面相同,只是改用 max(...) 函数:

float strength = max(abs(vUv.x - 0.5), abs(vUv.y - 0.5));

图案 19

对于这个图案,我们只是对上一步的值应用了 step(...):

float strength = step(0.2, max(abs(vUv.x - 0.5), abs(vUv.y - 0.5)));

图案 20

这个图案是把一个正方形与另一个更小且反转的正方形相乘。

float strength = step(0.2, max(abs(vUv.x - 0.5), abs(vUv.y - 0.5)));
strength *= 1.0 - step(0.25, max(abs(vUv.x - 0.5), abs(vUv.y - 0.5)));

图案 21

对于这个图案,我们先把 vUv.x 乘以 10.0,用 floor(...) 函数向下取整,再除以 10.0,从而得到一个介于 0.0 和 1.0 之间的值:

float strength = floor(vUv.x * 10.0) / 10.0;

图案 22

和之前一样,我们可以通过相乘来组合不同的轴:

float strength = floor(vUv.x * 10.0) / 10.0 * floor(vUv.y * 10.0) / 10.0;

图案 23

实现这个图案比较复杂,因为 GLSL 中没有原生的随机函数。诀窍是得到一个难以预测到看起来像随机的值。

获得这种值的一种流行做法是使用以下函数:

float random(vec2 st)
{
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

我们向这个函数传入一个 vec2,得到一个伪随机值。

如果你想进一步了解这个函数,这里是来自 The Book of Shaders 的一个链接:https://thebookofshaders.com/10/

我们可以把这个函数加在 main 函数之外,并配合 vUv 使用它:

varying vec2 vUv;

float random(vec2 st)
{
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

void main()
{
    // ...

    float strength = random(vUv);

    // ...
}

使用这个随机函数时要小心。使用错误的数值会让随机结果中出现明显的形状。

图案 24

这个图案是前两个图案的组合。首先,我们创建一个名为 gridUv 的新 vec2 坐标,其值为取整后的结果:

vec2 gridUv = vec2(floor(vUv.x * 10.0) / 10.0, floor(vUv.y * 10.0) / 10.0);

然后,我们把这两个坐标用于 random 函数:

float strength = random(gridUv);

图案 25

这个图案源自上一个。为了得到这种倾斜效果,我们在创建 gridUv 时要把 vUv.x 加到 vUv.y 上:

vec2 gridUv = vec2(floor(vUv.x * 10.0) / 10.0, floor((vUv.y + vUv.x * 0.5) * 10.0) / 10.0);
float strength = random(gridUv);

图案 26

在这个图案中,离左下角越远,strength 就越亮。

实际上这就是 vUv 的长度。vUv 的值在左下角等于 0.0, 0.0,所以长度是 0.0;离那个角越远,它的长度就越大。

我们可以用 length(...) 函数获取一个向量(vec2、vec3 或 vec4)的长度:

float strength = length(vUv);

图案 27

取而代之,我们要获取 vUv 与平面中心之间的距离。因为平面的 UV 从 0.0, 0.0 到 1.0, 1.0,所以中心是 0.5, 0.5。我们将创建一个对应中心的 vec2,并用 distance(...) 函数求出从 vUv 到该中心的距离:

float strength = distance(vUv, vec2(0.5));

当只用一个值创建向量时,这个值会赋给每一个分量——在我们的情况下就是 x 和 y。

要知道,我们也本可以偏移 vUv 并使用 length(...) 函数。

图案 28

对于这个图案,我们用 1.0 减去上一步的值:

float strength = 1.0 - distance(vUv, vec2(0.5));

图案 29

这个图案在创建光晕透镜效果时很有用。为了得到这个结果,我们从一个很小的值出发,把它除以上一步计算出的距离:

float strength = 0.015 / (distance(vUv, vec2(0.5)));

图案 30

这个图案相同,只是把 UV 压缩并仅在 y 轴上移动:

float strength = 0.15 / (distance(vec2(vUv.x, (vUv.y - 0.5) * 5.0 + 0.5), vec2(0.5)));

图案 31

而这个图案是用相同的公式相乘而来的,只是第二个基于 x 轴:

float strength = 0.15 / (distance(vec2(vUv.x, (vUv.y - 0.5) * 5.0 + 0.5), vec2(0.5)));
strength *= 0.15 / (distance(vec2(vUv.y, (vUv.x - 0.5) * 5.0 + 0.5), vec2(0.5)));

图案 32

实现这个图案相当费力。我们需要让 vUv 坐标围绕中心旋转。做 2D 旋转是 cos(...) 与 sin(...) 的组合,这里我们不再展开。这也是使用函数的好机会。把下面这个函数加在 main 函数之前:

vec2 rotate(vec2 uv, float rotation, vec2 mid)
{
    return vec2(
      cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,
      cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
    );
}

然后,我们可以用它创建一组新的 UV,命名为 rotatedUV。问题在于我们想正好旋转整个圆的八分之一。遗憾的是,GLSL 中无法访问 π(pi)。

取而代之,我们可以创建一个包含 π 近似值的变量:

float pi = 3.1415926535897932384626433832795;

因为这个变量永远不会改变,我们可以在代码开头把它保存为一个 define:

#define PI 3.1415926535897932384626433832795

define 比变量开销更低,但不能被修改。把 define 写成大写以区别于其他变量是一种好习惯。

然后我们可以把这个 PI 值用作 rotate(...) 函数的第二个参数(角度):

vec2 rotatedUv = rotate(vUv, PI * 0.25, vec2(0.5));

最后,我们用这个新的 rotatedUV 替换掉 vUv:

float strength = 0.15 / (distance(vec2(rotatedUv.x, (rotatedUv.y - 0.5) * 5.0 + 0.5), vec2(0.5)));
strength *= 0.15 / (distance(vec2(rotatedUv.y, (rotatedUv.x - 0.5) * 5.0 + 0.5), vec2(0.5)));

图案 33

为了绘制那个圆盘,我们把 distance(...) 函数与 step(...) 函数结合使用,并应用一个偏移来控制圆盘的半径:

float strength = step(0.5, distance(vUv, vec2(0.5)) + 0.25);

我们也本可以改变 step(...) 的第一个参数——名为边值(edge)——来控制半径。

图案 34

这个图案与上一个非常接近,只是我们使用 abs(...) 函数来保持值为正:

float strength = abs(distance(vUv, vec2(0.5)) - 0.25);

图案 35

我们可以组合前两个图案来得到一个圆:

float strength = step(0.02, abs(distance(vUv, vec2(0.5)) - 0.25));

图案 36

我们可以用 1.0 - ... 把它反转:

float strength = 1.0 - step(0.01, abs(distance(vUv, vec2(0.5)) - 0.25));

图案 37

这个图案基于上一个,但带有波浪状的扭曲。为了得到这个结果,我们创建一个新的 UV 变量,命名为 wavedUv,并把一个基于 x 轴的 sin(...) 加到 y 值上:

vec2 wavedUv = vec2(
    vUv.x,
    vUv.y + sin(vUv.x * 30.0) * 0.1
);

然后,我们用那个 wavedUv 取代 vUv:

float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

图案 38

对于这个图案,我们也把波浪扭曲应用到 x 轴上:

vec2 wavedUv = vec2(
    vUv.x + sin(vUv.y * 30.0) * 0.1,
    vUv.y + sin(vUv.x * 30.0) * 0.1
);
float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

图案 39

我们只需提高 sin(...) 的频率,就能得到迷幻效果:

vec2 wavedUv = vec2(
    vUv.x + sin(vUv.y * 100.0) * 0.1,
    vUv.y + sin(vUv.x * 100.0) * 0.1
);
float strength = 1.0 - step(0.01, abs(distance(wavedUv, vec2(0.5)) - 0.25));

想象一下把它做成动画的样子。

图案 40

这个图案实际上就是 vUv 的角度。要从 2D 坐标得到角度,我们可以使用 atan(...):

float angle = atan(vUv.x, vUv.y);
float strength = angle;

图案 41

这个图案相同,只是对 vUv 加了 0.5 的偏移,以创建围绕中心的角度:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
float strength = angle;

图案 42

再一次,这个图案相同,只是角度从 0.0 到 1.0。目前 atan(...) 返回一个介于 -π 和 +π 之间的值。首先,我们可以除以 PI * 2:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
angle /= PI * 2.0;
float strength = angle;

我们得到一个从 -0.5 到 0.5 的值。我们只需加上 0.5:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5);
angle /= PI * 2.0;
angle += 0.5;
float strength = angle;

拥有一个正确的角度是玩转圆形图案的好办法。我们会把 angle 的运算合并到一行中,以便更易阅读:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (PI * 2.0) + 0.5;

图案 43

这个图案基于我们一开始用取模所做的相同技巧,只是这次作用于 angle:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (PI * 2.0) + 0.5;
float strength = mod(angle * 20.0, 1.0);

图案 44

而这个使用了 sin(...):

float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (PI * 2.0) + 0.5;
float strength = sin(angle * 100.0);

图案 45

我们可以用上一步的值来定义我们之前画的圆的半径:

float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (PI * 2.0) + 0.5;
float radius = 0.25 + sin(angle * 100.0) * 0.02;
float strength = 1.0 - step(0.01, abs(distance(vUv, vec2(0.5)) - radius));

图案 46

这个图案叫做 perlin noise。你很可能已经听说过它,如果没有,你也可能在不经意间见过它。perlin noise 在重现云、水、火、地形高低等自然形态时非常关键,但它也可以用来让草或雪在风中摆动。

perlin noise 有许多种算法,结果各异,维度也不同(2D、3D 甚至 4D),有些会自我重复,有些性能更高,等等。

这里有一个 Github gist,列出了我们能找到的一些最流行的 GLSL perlin noise:https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83

不过要小心;有些代码可能不会立刻生效,我们马上就会看到。现在我们将测试 Stefan Gustavson 的 Classic Perlin Noise,它是一个 2D noise——我们传入一个 vec2,返回一个 float。先把代码复制到你的 shader 里,但先不要使用它:

//	Classic Perlin 2D Noise 
//	by Stefan Gustavson
//
vec2 fade(vec2 t)
{
    return t*t*t*(t*(t*6.0-15.0)+10.0);
}

float cnoise(vec2 P)
{
    vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
    vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
    Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation
    vec4 ix = Pi.xzxz;
    vec4 iy = Pi.yyww;
    vec4 fx = Pf.xzxz;
    vec4 fy = Pf.yyww;
    vec4 i = permute(permute(ix) + iy);
    vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024...
    vec4 gy = abs(gx) - 0.5;
    vec4 tx = floor(gx + 0.5);
    gx = gx - tx;
    vec2 g00 = vec2(gx.x,gy.x);
    vec2 g10 = vec2(gx.y,gy.y);
    vec2 g01 = vec2(gx.z,gy.z);
    vec2 g11 = vec2(gx.w,gy.w);
    vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
    g00 *= norm.x;
    g01 *= norm.y;
    g10 *= norm.z;
    g11 *= norm.w;
    float n00 = dot(g00, vec2(fx.x, fy.x));
    float n10 = dot(g10, vec2(fx.y, fy.y));
    float n01 = dot(g01, vec2(fx.z, fy.z));
    float n11 = dot(g11, vec2(fx.w, fy.w));
    vec2 fade_xy = fade(Pf.xy);
    vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
    float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
    return 2.3 * n_xy;
}

不幸的是,这段代码似乎让 shader 出了问题,这是因为缺少一个名为 permute 的函数。它在这里,你可以把它加在 fade 函数之前:

vec4 permute(vec4 x)
{
    return mod(((x*34.0)+1.0)*x, 289.0);
}

现在我们可以访问 cnoise 函数了,可以对它使用 vUv:

float strength = cnoise(vUv);

结果很粗糙,但我们总算有了点东西。想看到预览里那样更多的图案,就把 vUv 乘以 10.0:

float strength = cnoise(vUv * 10.0);

图案 47

这个图案使用相同的 noise,只是在上面加了一个 step:

float strength = step(0.0, cnoise(vUv * 10.0));

如果哪天你想做一头奶牛,这会非常有用。

图案 48

对于这个图案,我们对值使用了 abs(...),然后用 1.0 减去结果:

float strength = 1.0 - abs(cnoise(vUv * 10.0));

你可以用它来做闪电、水下倒影或等离子能量之类的东西。

图案 49

对于这个图案,我们对 noise 应用了 sin(...):

float strength = sin(cnoise(vUv * 10.0) * 20.0);

图案 50

而对于这最后一个,我们把 sin(...) 和 step(...) 组合起来:

float strength = step(0.9, sin(cnoise(vUv * 10.0) * 20.0));

小菜一碟,黑白分明,轻松搞定。

用颜色测试

这很有趣,但这些黑白颜色开始变得单调了。我们一开始有一个很酷的颜色,就是把 vUv 直接用在 gl_FragColor 中:

gl_FragColor = vec4(vUv, 1.0, 1.0);

现在我们可以做的就是用那个渐变色取代白色。

混合颜色

为此,我们将使用 mix(...) 函数。这个函数需要 3 个参数:

让我们创建第一个颜色:

vec3 blackColor = vec3(0.0);

让我们构造第二个颜色:

vec3 uvColor = vec3(vUv, 1.0);

我们根据 strength 得到两个颜色之间的混合:

vec3 mixedColor = mix(blackColor, uvColor, strength);

然后我们在 gl_FragColor 中使用这个混合结果,不改动 alpha:

gl_FragColor = vec4(mixedColor, 1.0);

尽情用它测试之前所有的图案吧。

修正 strength

如果你用这个 UV 渐变测试 #11、#14 和 #15 等图案,会在相交处看到一些奇怪的表现。

看起来相交处太亮了,而事实确实如此。这是因为我们在 mix(...) 中使用的 strength 值高于 1.0,输出就被外推了——意思是它超出了第二个值。

为了限制这个值,我们可以对 strength 使用 clamp(...) 函数。这个函数只是给一个值设定下限和上限:

// Pattern 11
float strength = step(0.8, mod(vUv.x * 10.0, 1.0));
strength += step(0.8, mod(vUv.y * 10.0, 1.0));
strength = clamp(strength, 0.0, 1.0);

// ...

// Pattern 14
float barX = step(0.4, mod(vUv.x * 10.0, 1.0)) * step(0.8, mod(vUv.y * 10.0, 1.0));
float barY = step(0.8, mod(vUv.x * 10.0, 1.0)) * step(0.4, mod(vUv.y * 10.0, 1.0));
float strength = barX + barY;
strength = clamp(strength, 0.0, 1.0);

// Pattern 15
float barX = step(0.4, mod(vUv.x * 10.0 - 0.2, 1.0)) * step(0.8, mod(vUv.y * 10.0, 1.0));
float barY = step(0.8, mod(vUv.x * 10.0, 1.0)) * step(0.4, mod(vUv.y * 10.0 - 0.2, 1.0));
float strength = barX + barY;
strength = clamp(strength, 0.0, 1.0);

进一步探索

还有许多其他可能的图案和许多额外的函数。这一课的目的是为你未来的项目备好更多技能,并在一个简单的场景中练习 GLSL。

有一件有用的事我们没有尝试,就是把这些形状封装进函数里。我们本可以创建 getCircle 函数、getSquare 函数等,并配上合适的参数,以便轻松复用。

继续练习,不要害怕创建新的形状,多做实验,需要时就去寻求帮助。

另外,试试添加一些 uniforms 来给这些值做动画,或在调试面板中加一些调节项。